<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');

// ------------------------------------------------------------------------

/**
 * Router Class
 */
class CI_Router {

    /**
     * Config class
     */
    var $config;

    /**
     * List of routes

     */
    var $routes = array();

    /**
     * List of error routes
     */
    var $error_routes = array();

    /**
     * Current class name
     */
    var $class = '';

    /**
     * Current method name
     */
    var $method = 'index';

    /**
     * Sub-directory that contains the requested controller class
     */
    var $directory = '';

    /**
     * Default controller (and method if specific)
     */
    var $default_controller;

    /**
     * Constructor
     */
    function __construct()
    {
        $this->config =& load_class('Config', 'core');
        $this->uri =& load_class('URI', 'core');
        log_message('debug', "Router Class Initialized");
    }

// --------------------------------------------------------------------

    /**
     * Set the route mapping
     */
    function _set_routing()
    {

//如果项目是允许通过query_strings的形式，并且有通过$_GET的方式请求控制器的话，则以query_string形式，也就是
//?c=xx的形式确定路由。
        $segments = array();
        if ($this->config->item('enable_query_strings') === TRUE AND isset($_GET[$this->config->item('controller_trigger')]))
        {
//上面这里为什么还要判断有没有通过get的方式指定控制器？其实是因为如果允许query_string的形式请求路由，但是却
//没有通过query_string（或者说是get）的形式指定路由的话（其实就说明这个通过query_string方式的uri是无效的），
//此时,我们依然会采用“段”的形式。

//取得目录名,目录名，控制名和方法名传递的变量名都是可以自定义的，在config/config.php里面。
            if (isset($_GET[$this->config->item('directory_trigger')]))
            {
                $this->set_directory(trim($this->uri->_filter_uri($_GET[$this->config->item('directory_trigger')])));
                $segments[] = $this->fetch_directory();
            }

//取得控制器名
            if (isset($_GET[$this->config->item('controller_trigger')]))
            {
                $this->set_class(trim($this->uri->_filter_uri($_GET[$this->config->item('controller_trigger')])));
                $segments[] = $this->fetch_class();
            }

//取得方法名
            if (isset($_GET[$this->config->item('function_trigger')]))
            {
                $this->set_method(trim($this->uri->_filter_uri($_GET[$this->config->item('function_trigger')])));
                $segments[] = $this->fetch_method();
            }
        }
//。。。。。。。。。。。。。。。位置1

// Load the routes.php file.
//引入关于路由方面的配置信息。配置文件里面是一个名字为$route的数组。
        if (defined('ENVIRONMENT') AND is_file(APPPATH.'config/'.ENVIRONMENT.'/routes.php'))
        {
            include(APPPATH.'config/'.ENVIRONMENT.'/routes.php');
        }
        elseif (is_file(APPPATH.'config/routes.php'))
        {
            include(APPPATH.'config/routes.php');
        }

//下面这个莫名出现的$route变量(注意不是$this->routes哦)，就是写在配置文件里面。把它copy到$this->routes。
//这个$routes是用来指定默认控制器和默认方法，404（请求路由不存在）后规定的路由以及一些路由重定向（？这个重写向的
//实现在Router::_parse_routes()中实现）的信息。
        $this->routes = ( ! isset($route) OR ! is_array($route)) ? array() : $route;
        unset($route);//利用完就干掉。

//根据刚才的配置信息，设定默认控制器，没有的话，就为FLASE。
        $this->default_controller = ( ! isset($this->routes['default_controller']) OR $this->routes['default_controller'] == '') ? FALSE : strtolower($this->routes['default_controller']);

//这个判断的位置放得有点怪，我觉得可以放到上面“位置1”的地方，下面的代码是判断刚才有没有通过query_string的方式拿到
//路由信息，如果拿到的话，那么就不再尝试“段”的方式确定路由了。直接确定路由，结束本函数。
        if (count($segments) > 0)
        {
//_validate_quest($segments);的作用就是确定并设置路由。
//这个函数执行完之后，Router::$class,Router::$directory（如果有）都会有相应值。
            return $this->_validate_request($segments);
        }

//下面的_fetch_uri_string()详见：URI.php，在这个位置只需知道它的作用是：
//从uri中检测处理，把我们确定路由需要的信息（例如“index.php/index/welcome/1”后
//面"index/welcome/1"这串）放到$this->uri->uri_string这个东西中。
        $this->uri->_fetch_uri_string();

//上面_fetch_uri_string()完了之后，这个uri_string就会有我们要用的信息，如果为空的话，那么就用把路由设置为默认的。
        if ($this->uri->uri_string == '')
        {
//移步至Router::_set_default_controller();
            return $this->_set_default_controller();
        }

//如果$this->uri->uri_string 不为空，那么，会通过下面的方式确定路由

//这里只是简单地把后缀去掉而已，因为CI允许在uri后面加后缀，但它其实对我们寻找路由是多余，而且会造成影响的，所以先去掉。
        $this->uri->_remove_url_suffix();

//把最初的uri，变成数组放在segments里面。
        $this->uri->_explode_segments();

//开始找路由。移步至 Router::_parse_routes();
        $this->_parse_routes();

//设置为由1开始。
        $this->uri->_reindex_segments();
    }

// --------------------------------------------------------------------

    /**
     * Set the default controller
     */
    function _set_default_controller()
    {
//在Router::_set_routing()函数里面有一个操作，是从配置文件里面读取默认控制器名，如果没有就有FALSE。
        if ($this->default_controller === FALSE)
        {
//如果没有默认的话，就报错，结束程序。
//实质上，这个_set_default_controller()仅仅是在uri没有指定控制器，要求访问默认控制器的时候才
//被调用，所以如果连默认控制器都没有，那么可以果断报错。
            show_error("Unable to determine what should be displayed. A default route has not been specified in the routing file.");
        }

//如果有，下面我们就来把默认的控制器设置为当前要找的路由。

//这里只是分“有指定默认方法”和“没有指定”两种情况而已。不过要弄点下面那个$this->_set_request($x);CI这几个函数
//也许写得很妙，但是让人看得纠结。。。
        if (strpos($this->default_controller, '/') !== FALSE)
        {
            $x = explode('/', $this->default_controller);

            $this->set_class($x[0]);
            $this->set_method($x[1]);
            $this->_set_request($x);//移步至Router::_set_request();
        }
        else
        {
            $this->set_class($this->default_controller);
            $this->set_method('index');
            $this->_set_request(array($this->default_controller, 'index'));
        }

// re-index the routed segments array so it starts with 1 rather than 0
        $this->uri->_reindex_segments();

        log_message('debug', "No URI present. Default controller set.");
    }

// --------------------------------------------------------------------

    /**
     * Set the Route
     */
    function _set_request($segments = array())
    {
        /**
         * 下面来解剖一下这个让人纠结的函数。。第一次看的时候差点被它们这几个函数搞晕。
         */

        /**
         * 看，这里有调用Router::_validate_request();而Router::_validate_request()的作用是检测寻找出一个
         * 正确存在的路由，并确定它，确定后的值分别放到Rouer::$class这些属性里面。所以使到这个_set_request()也有
         * 这种确定路由的功能。
         *
         * 注：
         * $segments=$this->_validate_request($segments); 等式右边，括号里面的这个$segments，也就是调用
         * _set_request()时传入来的这个参数，它有这样的特点：
         * 1）如果这时_set_request()是在Router::_set_default_controller()中调用的话，那个这个$segments是永远不会为
         * 空数组，嗯，绝对不会。
         *
         *
         * 而左边这个$segments的值，经过下面这行代码后，要么为空数组array(),要么为确定路由后的段数组。
         * 为空数组的原因是，$this->_validate_request();里面没有找到当前目录的默认控制器。此时，右边的
         * $segments要么为空，要么只指定了目录但默认控制器不存在。
         */
        $segments = $this->_validate_request($segments);

        if (count($segments) == 0)
        {
//所以如果上面返回了空数组，就会进到这里。
//这里居然又调回了_set_default_controller()! 坑爹吧！
            return $this->_set_default_controller();
            /**
             * 我曾经想过，下面这里会不会死循环：
             * 假如，我在配置文件里面的默认控制器设为welcome，然后controllers/下没有welcome.php,但controllers/下有
             * welcome/有这个目录（里面没东西），然后通过http://localhost/CI/来访问默认控制器，那会怎样呢？
             * 首先，它会进入_set_routing();然后发现$this->uri->uri_string为空，进入_set_default_controller();
             * 然后发现在_set_default_controller里，发现$this->default_controller不为FALSE，（＠＠＠＠），然后再
             * 进入这_set_request()里面，再进入_validate_request()里面，会不会_validate_request里返回空数组？因为
             * 指定了目录，没有指定控制器，访问默认的，又不存在，然后返回空数组，返回空数组后，最终就会走来你正在看的这个位置，
             * 然后这个位置再调用_set_default_controller();然后死循环了。。。
             *
             * 答案是不会的。
             * 原因在于：
             * 我们回到上面解译那个（＠＠＠＠）的地方，在这里，发现$this->default_controller不为FALSE后，它会进入这个else
             * 里面
             * else
             * {
             * $this->set_class($this->default_controller); ..............1
             * $this->set_method('index'); ...................2
             * $this->_set_request(array($this->default_controller, 'index')); ..........3
             * }
             *
             * 然后第3行，传入_set_request($segments)中的那个$segments其实是
             * array('welcome','index')，重点在于那个小小的'index'!!!!!!!
             * 这样一来，我们进入_validate_request()的时候，我们实质并没有“指定目录但没有指定控制器，访问默认控制器”，
             * 而是“指定了一个welcome的目录，和一个叫index的控制器！！”，所以才不会死循环。
             * 如果你试着把第3行那个'index'去掉，那么，一定会死循环！！！！！！！！不信试试！CI太牛逼了，居然这样做。汗。。
             * 当然，‘index’还有一个作用，就是设置默认方法啦。
             */
        }

        $this->set_class($segments[0]);

        if (isset($segments[1]))
        {
// A standard method request
            $this->set_method($segments[1]);
        }
        else
        {
            $segments[1] = 'index';
        }

//这里要说一下，现在是在ROUTER里面为URI赋值，URI里面的这个URI::$rsegments是经过处理，并确定路由后，实质调用的路由的段信息。
//而URI::$segments （前面少了个r），则是原来没处理前的那个，即直接由网址上面得出来的那个。
        $this->uri->rsegments = $segments;
    }

// --------------------------------------------------------------------

    /**
     * Validates the supplied segments. Attempts to determine the path to
     * the controller.
     */
    function _validate_request($segments)
    {
        if (count($segments) == 0)
        {
            return $segments;
        }

        if (file_exists(APPPATH.'controllers/'.$segments[0].'.php'))
        {
//如果直接在controllers这个目录下找到与第一段相应的控制器名，那就说明找到了控制器，确定路由，返回。
            return $segments;
        }

//如果上面没有找到，再看看这个“第一段”是不是一个目录，因为CI是允许控制器放在自定义的目录下的。
        if (is_dir(APPPATH.'controllers/'.$segments[0]))
        {
// Set the directory and remove it from the segment array
//如果的确是目录，那么就可以确定路由的目录部分了。
            $this->set_directory($segments[0]);
//去掉目录部分。进一步进行路由寻找。
            $segments = array_slice($segments, 1);

//如果uri请求中除了目录还有其它“段”，那说明是有请求某指定控制器的。
            if (count($segments) > 0)
            {
//指定请求的控制器找不到的话，那只好报错了。
                if ( ! file_exists(APPPATH.'controllers/'.$this->fetch_directory().$segments[0].'.php'))
                {
//报错也有两方式，一种是默认的，一种是自义定的。
//下面这个404_override就是在config/routes.php定义的一个路由找不到时候的默认处理控制器了，如果有定义
//我们调用它。
                    if ( ! empty($this->routes['404_override']))
                    {
                        $x = explode('/', $this->routes['404_override']);

                        $this->set_directory('');//把刚才设置好的路由的目录部分去掉，因为现在路由是我们定义的404路由。
                        $this->set_class($x[0]);//这里可以看出，我们定义的404路由是不允许放在某个目录下的，只能直接放在controllers/下
                        $this->set_method(isset($x[1]) ? $x[1] : 'index');//默认是index方法

                        return $x; //同样，返回“段”数组
                    }
                    else
                    {
//默认找不到路由的方法。在core/Common.php中定义的全局函数（实质调用Exception组件进行处理）。
                        show_404($this->fetch_directory().$segments[0]);
                    }
                }
            }
            else
            {
//来到这里，说明了是uri请求指定了目录，而没有指定控制器的情况下。那么，我们默认当前路由是在当前目录下请求默认的
//控制器和方法。

//下面这个判断只是判断一下$this->default_controller有没有指定方法而已。
                if (strpos($this->default_controller, '/') !== FALSE)
                {
                    $x = explode('/', $this->default_controller);
                    $this->set_class($x[0]);
                    $this->set_method($x[1]);
                }
                else
                {
                    $this->set_class($this->default_controller);
                    $this->set_method('index');//没有的话就默认为index方法。
                }

// Does the default controller exist in the sub-folder?
//如果连默认控制器都不存在的话，就无语了，说明uri打算请求这个目录的默认控制器，结果没有这个默认控制器，那暂时
//返回个空数组。（但是看清楚，上面已经$this->set_class()了，说明即使没有，我们也已经把默认控制器的名字算
//是确定下来先）
                if ( ! file_exists(APPPATH.'controllers/'.$this->fetch_directory().$this->default_controller.'.php'))
                {
                    $this->directory = '';
                    return array();
                }

            }

//能够有命来到这一步，是说明的确是某个目录下找到了控制器，或者是找到了定义的默认控制器。
//但是注意，这个$segments返回的“段”信息都是不包括目录的。它是一个数组形式，第一个元素是控制器名。
//例如：array('acontroller','amethod','xx','xx')。。
            return $segments;
        }

//来到这里，就说明了，即找不到controllers/下相应的控制器，也找不到这样的目录。那就报错咯。
        if ( ! empty($this->routes['404_override']))
        {
            $x = explode('/', $this->routes['404_override']);

            $this->set_class($x[0]);
            $this->set_method(isset($x[1]) ? $x[1] : 'index');

            return $x;
        }

// Nothing else to do at this point but show a 404
        show_404($segments[0]);
    }

// --------------------------------------------------------------------

    /**
     * Parse Routes
     */
    function _parse_routes()
    {
// Turn the segment array into a URI string
//知道_set_request()是干嘛的之后，下面的条理就比较清晰了。
        $uri = implode('/', $this->uri->segments);

// Is there a literal match? If so we're done
        if (isset($this->routes[$uri]))
        {
            return $this->_set_request(explode('/', $this->routes[$uri]));
        }

        /**
         * CI有路由重定向的功能，重定向的规则和实现就是在这里。
         */
        foreach ($this->routes as $key => $val)
        {
            $key = str_replace(':any', '.+', str_replace(':num', '[0-9]+', $key));

            if (preg_match('#^'.$key.'$#', $uri))
            {
// Do we have a back-reference?
                if (strpos($val, '$') !== FALSE AND strpos($key, '(') !== FALSE)
                {
                    $val = preg_replace('#^'.$key.'$#', $val, $uri);
                }

                return $this->_set_request(explode('/', $val));
            }
        }
        $this->_set_request($this->uri->segments);
    }

// --------------------------------------------------------------------

    /**
     * Set the class name
     */
    function set_class($class)
    {
        $this->class = str_replace(array('/', '.'), '', $class);
    }

// --------------------------------------------------------------------

    /**
     * Fetch the current class
     */
    function fetch_class()
    {
        return $this->class;
    }

// --------------------------------------------------------------------

    /**
     * Set the method name
     */
    function set_method($method)
    {
        $this->method = $method;
    }

// --------------------------------------------------------------------

    /**
     * Fetch the current method
     */
    function fetch_method()
    {
        if ($this->method == $this->fetch_class())
        {
            return 'index';
        }

        return $this->method;
    }

// --------------------------------------------------------------------

    /**
     * Set the directory name
     */
    function set_directory($dir)
    {
        $this->directory = str_replace(array('/', '.'), '', $dir).'/';
    }

// --------------------------------------------------------------------

    /**
     * Fetch the sub-directory (if any) that contains the requested controller class
     */
    function fetch_directory()
    {
        return $this->directory;
    }

// --------------------------------------------------------------------

    /**
     * Set the controller overrides
     */
    function _set_overrides($routing)
    {
        if ( ! is_array($routing))
        {
            return;
        }

        if (isset($routing['directory']))
        {
            $this->set_directory($routing['directory']);
        }

        if (isset($routing['controller']) AND $routing['controller'] != '')
        {
            $this->set_class($routing['controller']);
        }

        if (isset($routing['function']))
        {
            $routing['function'] = ($routing['function'] == '') ? 'index' : $routing['function'];
            $this->set_method($routing['function']);
        }
    }

}